﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Reflection;
using System.Windows.Forms;

namespace System.ComponentModel.Design;

/// <summary>
///  This class provides a default implementation of the event
///  binding service.
/// </summary>
public abstract partial class EventBindingService : IEventBindingService
{
    private readonly IServiceProvider _provider;
    private IComponent? _showCodeComponent;
    private EventDescriptor? _showCodeEventDescriptor;
    private string? _showCodeMethodName;

    /// <summary>
    ///  You must provide a service provider to the binding
    ///  service.
    /// </summary>
    protected EventBindingService(IServiceProvider provider)
    {
        _provider = provider.OrThrowIfNull();
    }

    /// <summary>
    ///  Creates a unique method name.  The name must be
    ///  compatible with the script language being used and
    ///  it must not conflict with any other name in the user's
    ///  code.
    /// </summary>
    protected abstract string CreateUniqueMethodName(IComponent component, EventDescriptor e);

    /// <summary>
    ///  This provides a notification that a particular method
    ///  is no longer being used by an event handler.  Some implementations
    ///  may want to remove the event hander when no events are using
    ///  it.  By overriding UseMethod and FreeMethod, an implementation
    ///  can know when a method is no longer needed.
    /// </summary>
    protected virtual void FreeMethod(IComponent component, EventDescriptor e, string methodName)
    { }

    /// <summary>
    ///  Returns a collection of strings.  Each string is
    ///  the method name of a method whose signature is
    ///  compatible with the delegate contained in the
    ///  event descriptor.  This should return an empty
    ///  collection if no names are compatible.
    /// </summary>
    protected abstract ICollection GetCompatibleMethods(EventDescriptor e);

    /// <summary>
    ///  Gets the requested service from our service provider.
    /// </summary>
    protected object? GetService(Type serviceType) => _provider.GetService(serviceType);

    /// <summary>
    ///  Shows the user code.  This method does not show any
    ///  particular code; generally it shows the last code the
    ///  user typed.  This returns true if it was possible to
    ///  show the code, or false if not.
    /// </summary>
    protected abstract bool ShowCode();

    /// <summary>
    ///  Shows the user code at the given line number.  Line
    ///  numbers are one-based.  This returns true if it was
    ///  possible to show the code, or false if not.
    /// </summary>
    protected abstract bool ShowCode(int lineNumber);

    /// <summary>
    ///  Shows the body of the user code with the given method
    ///  name. This returns true if it was possible to show
    ///  the code, or false if not.
    /// </summary>
    protected abstract bool ShowCode(IComponent component, EventDescriptor e, string methodName);

    /// <summary>
    ///  This provides a notification that a particular method
    ///  is being used by an event handler.  Some implementations
    ///  may want to remove the event hander when no events are using
    ///  it.  By overriding UseMethod and FreeMethod, an implementation
    ///  can know when a method is no longer needed.
    /// </summary>
    protected virtual void UseMethod(IComponent component, EventDescriptor e, string methodName)
    { }

    /// <summary>
    ///  This validates that the provided method name is valid for
    ///  the language / script being used.  The default does nothing.
    ///  You may override this and throw an exception if the name
    ///  is invalid for your use.
    /// </summary>
    protected virtual void ValidateMethodName(string methodName)
    { }

    /// <summary>
    ///  This creates a name for an event handling method for the given component
    ///  and event.  The name that is created is guaranteed to be unique in the user's source
    ///  code.
    /// </summary>
    string IEventBindingService.CreateUniqueMethodName(IComponent component, EventDescriptor e)
    {
        ArgumentNullException.ThrowIfNull(component);
        ArgumentNullException.ThrowIfNull(e);

        return CreateUniqueMethodName(component, e);
    }

    /// <summary>
    ///  Retrieves a collection of strings.  Each string is the name of a method
    ///  in user code that has a signature that is compatible with the given event.
    /// </summary>
    ICollection IEventBindingService.GetCompatibleMethods(EventDescriptor e)
    {
        ArgumentNullException.ThrowIfNull(e);

        return GetCompatibleMethods(e);
    }

    /// <summary>
    ///  For properties that are representing events, this will return the event
    ///  that the property represents.
    /// </summary>
    EventDescriptor? IEventBindingService.GetEvent(PropertyDescriptor property)
    {
        return (property as EventPropertyDescriptor)?.Event;
    }

    /// <summary>
    ///  Returns true if the given event has a generic argument or return value in its raise method.
    /// </summary>
    private static bool HasGenericArgument([NotNullWhen(true)] EventDescriptor? ed)
    {
        if (ed is null || ed.ComponentType is null)
        {
            return false;
        }

        EventInfo? evInfo = ed.ComponentType.GetEvent(ed.Name);

        if (evInfo is null || !evInfo.EventHandlerType!.IsGenericType)
        {
            return false;
        }

        Type[] args = evInfo.EventHandlerType.GetGenericArguments();

        if (args is not null && args.Length > 0)
        {
            for (int i = 0; i < args.Length; i++)
            {
                if (args[i].IsGenericType)
                {
                    return true;
                }
            }
        }

        return false;
    }

    /// <summary>
    ///  Converts a set of events to a set of properties.
    /// </summary>
    PropertyDescriptorCollection IEventBindingService.GetEventProperties(EventDescriptorCollection events)
    {
        ArgumentNullException.ThrowIfNull(events);

        List<PropertyDescriptor> props = new(events.Count);

        // We cache the property descriptors here for speed.  Create those for
        // events that we don't have yet.
        for (int i = 0; i < events.Count; i++)
        {
            if (HasGenericArgument(events[i]))
            {
                continue;
            }

            PropertyDescriptor prop = new EventPropertyDescriptor(events[i]!, this);
            props.Add(prop);
        }

        return new PropertyDescriptorCollection([.. props]);
    }

    /// <summary>
    ///  Converts a single event to a property.
    /// </summary>
    PropertyDescriptor IEventBindingService.GetEventProperty(EventDescriptor e)
    {
        ArgumentNullException.ThrowIfNull(e);

        PropertyDescriptor prop = new EventPropertyDescriptor(e, this);

        return prop;
    }

    /// <summary>
    ///  Displays the user code for this designer.  This will return true if the user
    ///  code could be displayed, or false otherwise.
    /// </summary>
    bool IEventBindingService.ShowCode()
    {
        return ShowCode();
    }

    /// <summary>
    ///  Displays the user code for the designer.  This will return true if the user
    ///  code could be displayed, or false otherwise.
    /// </summary>
    bool IEventBindingService.ShowCode(int lineNumber)
    {
        return ShowCode(lineNumber);
    }

    /// <summary>
    ///  Displays the user code for the given event.  This will return true if the user
    ///  code could be displayed, or false otherwise.
    /// </summary>
    bool IEventBindingService.ShowCode(IComponent component, EventDescriptor e)
    {
        ArgumentNullException.ThrowIfNull(component);
        ArgumentNullException.ThrowIfNull(e);

        PropertyDescriptor prop = ((IEventBindingService)this).GetEventProperty(e);
        string? methodName = (string?)prop.GetValue(component);

        if (methodName is null)
        {
            return false;   // the event is not bound to a method.
        }

        Debug.Assert(_showCodeComponent is null && _showCodeEventDescriptor is null && _showCodeMethodName is null, "show code already pending");
        _showCodeComponent = component;
        _showCodeEventDescriptor = e;
        _showCodeMethodName = methodName;
        Application.Idle += new EventHandler(ShowCodeIdle);

        return true;
    }

    /// <summary>
    ///  Displays the user code for the given event.  This will return true if the user
    ///  code could be displayed, or false otherwise.
    /// </summary>
    private void ShowCodeIdle(object? sender, EventArgs e)
    {
        Application.Idle -= new EventHandler(ShowCodeIdle);

        try
        {
            ShowCode(_showCodeComponent!, _showCodeEventDescriptor!, _showCodeMethodName!);
        }
        finally
        {
            _showCodeComponent = null;
            _showCodeEventDescriptor = null;
            _showCodeMethodName = null;
        }
    }
}
